Julia has several means for inspection of the program execution:
Read sections Profiling and Reflection and introspection of the Julia manual (20 min).
The reader should be able to determine frequency of executed commands and memory allocation a function.
In [1]:
a=rand(1000000)
b=rand(1000000)
α=rand()
Out[1]:
In [2]:
@time α*a⋅b
Out[2]:
In [3]:
@time α*(a⋅b)
Out[3]:
We see that the second evaluation is faster, due to adequate memory allocation - the reason is that there is no operator precedence between * and $\cdot$. Also,
loops in Julia are very fast:
In [4]:
@time s=0.0; for i=1:1000000; s+=α*a[i]*b[i]; end; s
Out[4]:
The function code_llvm() and code_native() return the LLVM intermediate representation of a function and the compiled machine code, respectively. (There are other useful functions described in the Manual.)
They can also be called as macros, which is the form that we shall use.
Observe the differences in the small examples below:
In [5]:
?code_llvm
Out[5]:
In [6]:
?code_native
Out[6]:
In [7]:
@code_llvm +(1,2)
In [8]:
@code_llvm +(1.0,2)
In [9]:
@code_native +(1,2)
In [10]:
@code_native +(1.0,2)
We shall demonstrate the process on simple problem of polynomial evaluation. We shall use the registered package Polynomials.jl for polynomial manipulations, and two own functions for polynomial evalutaion:
In [11]:
# Pkg.add("Polynomials")
using Polynomials
In [12]:
whos(Polynomials)
In [13]:
# polyval() is just Horner's scheme
methods(polyval)
Out[13]:
function polyval{T,S}(p::Poly{T}, x::S)
R = promote_type(T,S)
lenp = length(p)
if lenp == 0
return zero(R) * x
else
y = convert(R, p[end])
for i = (endof(p)-1):-1:0
y = p[i] + x*y
end
return y
end
end
In [14]:
# Polynomial with given coefficients
p=Poly([1,2,3,4])
Out[14]:
In [15]:
# Polynomial with given zeros
q=poly([1,2,3,4])
Out[15]:
In [16]:
r=p*q
Out[16]:
In [17]:
typeof(r)
Out[17]:
In [18]:
p(pi), polyval(p,pi), q(pi), polyval(q,π)
Out[18]:
In [19]:
p[0]
Out[19]:
In [20]:
function mypolyval(p::Poly,x::Number)
s=p[0]
t=one(x)
for i=1:length(p)-1
t*=x
s+=p[i]*t
end
s
end
function myhorner(p::Poly,x::Number)
s=p[end]
for i=length(p)-2:-1:0
s*=x
s+=p[i]
end
s
end
Out[20]:
In [21]:
mypolyval(p,map(Float64,π)), myhorner(p,map(Float64,pi))
Out[21]:
Let us time the functions:
In [22]:
n=1000001
pbig=Poly(rand(n))
x=0.12345;
In [24]:
@time pbig(x)
Out[24]:
In [26]:
@time polyval(pbig,x)
Out[26]:
In [28]:
@time myhorner(pbig,x)
Out[28]:
In [30]:
# This is slightly faster
@time mypolyval(pbig,x)
Out[30]:
In [31]:
# Provided sufficient assembler knowledge, we can
# inspect the details
@code_native mypolyval(pbig,x)
In [33]:
?@profile
Out[33]:
In [34]:
Profile.clear()
In [35]:
@profile (for i = 1:100; mypolyval(pbig,x); end)
In [36]:
Profile.print()
In [37]:
Profile.clear()
@profile (for i = 1:100; myhorner(pbig,x); end)
Profile.print()
By inspecting the output, we see that the main load is the execution of computational lines inside the loops.
The above profiles also includes IJulia calls. It profiling is done in terminal mode, IJulia calls will not be present. This can be done with the following commands:
include("myfunctions.jl") # Contains the function definitions
Profile.clear()
@profile (for i = 1:100000; mypolyval(pbig,x); end)
Profile.print()
Profile.clear()
@profile (for i = 1:100000; myhorner(pbig,x); end)
Profile.print()
Output can also be viewed using the package ProfileView.jl.
In [38]:
# Pkg.add("ProfileView")
In [39]:
using ProfileView
In [40]:
ProfileView.view()
Out[40]:
Memory allocation analysis must be performed in terminal mode. The entire code must be stored in a single file, for example myfile.jl.
The command
julia --track-allocation=user myfile.jl
(or C:\Users\Ivan\AppData\Local\Julia-0.6.2\bin\julia --track-allocation=user myfile.jl)
generates the file myfile.jl.mem with memory allocation displayed for each line of code.
In [ ]: